package afcp.alumni.dao.base;

import java.lang.reflect.Field;
import java.util.List;

import javax.jdo.annotations.Extension;
import javax.jdo.annotations.PrimaryKey;
import javax.persistence.GeneratedValue;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import afcp.alumni.model.SearchResult;
import afcp.alumni.model.base.DatastoreEntity;
import afcp.alumni.search.DaoSearchAPI;
import afcp.alumni.search.SearchAPIResult;
import afcp.alumni.util.ReflectUtil;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;

public abstract class DaoDatastoreCommon<T extends DatastoreEntity> implements Dao<T> {
	
	private static final Log logger = LogFactory.getLog(DaoDatastoreCommon.class);

	protected abstract Class<T> getClassObject();

//	protected abstract Class<? extends UniqueField<?>> getUniqueFieldClassObject();

	protected Class<T> classObject = this.getClassObject();
//	protected Class<? extends UniqueField<?>> uniqueFieldClassObject = this
//			.getUniqueFieldClassObject();
	protected String className = classObject.getSimpleName();
//	protected String uniqueFieldClassName = uniqueFieldClassObject != null ? uniqueFieldClassObject
//			.getSimpleName() : null;

	protected DaoSearchAPI<T> sapi = new DaoSearchAPI<>(classObject);
	
	protected Field keyField;
	protected Field parentKeyField;
//	protected Field uniqueFieldKeyField;
//	protected Field uniqueFieldParentKeyField;

	protected DaoDatastoreCommon() {
		keyField = initKeyField(classObject, className);
		parentKeyField = initParentKeyField(classObject, className);
//		if (uniqueFieldClassObject != null) {
//			uniqueFieldKeyField = initKeyField(uniqueFieldClassObject, uniqueFieldClassName);
//			uniqueFieldParentKeyField = initParentKeyField(uniqueFieldClassObject, uniqueFieldClassName);
//		}
	}
	
	private static Field initKeyField(Class<?> classObject, String className) {
		Field keyField;
		List<Field> keyFields = ReflectUtil.getFieldsWithAnnotation(classObject,
				PrimaryKey.class);
		if (keyFields == null || keyFields.size() < 1) {
			throw new IllegalStateException("Key field has not been found in "
					+ "class " + className + " by the DAO.");
		}
		keyField = keyFields.get(0);
		if (!keyField.getType().equals(Key.class)) {
			throw new IllegalStateException("The key field is not of type "
					+ Key.class.getName());
		}
		return keyField;
	}
	
	private static Field initParentKeyField(Class<?> classObject, String className) {
		Field parentKeyField;
		List<Field> parentKeyFields = ReflectUtil.getFieldsWithAnnotation(
				classObject, Extension.class, "key",
				DatastoreEntity.ANNOTATION_PARENT_KEY_VALUE);
		if (parentKeyFields == null || parentKeyFields.size() < 1) {
			logger.info("No parent key field detected for model class [" + className + "].");
			parentKeyField = null;
		} else {
			parentKeyField = parentKeyFields.get(0);
		}
		return parentKeyField;
	}

//	protected abstract DatastoreEntity getEntityAncestor();

	protected Key getAncestorKey() {
		DatastoreEntity entityAncestor = getEntityAncestor();
		return entityAncestor != null ? entityAncestor.getKey() : null;
	}

	protected <U extends DatastoreEntity> void setKey(U entity) {
		boolean generatedId = keyField.getAnnotation(GeneratedValue.class) != null;
		if (entity != null && !generatedId) {
			// entity.setKeyWithAncestor(getAncestorKey());
			
			try {
//				if (entity instanceof UniqueField) {
//					setKey(entity, uniqueFieldKeyField,
//							uniqueFieldParentKeyField);
//				} else {
					setKey(entity, keyField, parentKeyField);
//					if (entity instanceof WithUniqueField) {
//						try {
//							setKey(((WithUniqueField) entity).getUniqueField(),
//									uniqueFieldKeyField,
//									uniqueFieldParentKeyField);
//						} catch (IllegalArgumentException e) {
//							logIllegalArgumentException(entity,
//									uniqueFieldKeyField, e);
//						} catch (IllegalAccessException e) {
//							logIllageAccessException(uniqueFieldKeyField, e);
//						}
//					}
//				}
//			} catch (NoSuchFieldException e) {
//				logger.error("Bad implementation. The field " + fieldName
//						+ " does not exist.", e);
//			} catch (SecurityException e) {
//				logger.error("The field " + fieldName
//						+ " can't be loaded or set to modifiable.", e);
			} catch (IllegalArgumentException e) {
				logIllegalArgumentException(entity, keyField, e);
			} catch (IllegalAccessException e) {
				logIllageAccessException(keyField, e);
			}
		}
	}

	private <U extends DatastoreEntity> void logIllegalArgumentException(
			U entity, Field field, IllegalArgumentException e) {
		logger.error("Error with object class name while trying "
				+ "to set the id with reflection for entity " + entity + ".");
		logger.error("The object is of class " + entity.getClass().getName()
				+ " but the field belongs to another object: "
				+ field.getDeclaringClass().getName(), e);
	}

	private void logIllageAccessException(Field field, IllegalAccessException e) {
		logger.error("The field " + field.getName() + " can't be modified.", e);
	}
	
	private <U extends DatastoreEntity> void setKey(U entity, Field keyField,
			Field parentKeyField) throws IllegalArgumentException,
			IllegalAccessException {

		if (entity == null) {
			logger.warn("Try to define the key of a null entity.");
			return;
		}
		
		Key ancestorKey = getAncestorKey();
		String entityName = entity.getClass().getSimpleName();
		String entityId = entity.getId();
		// If the Id is non-null, the key is generated from it.
		boolean generatedId = keyField.getAnnotation(GeneratedValue.class) != null;
		if (!generatedId) {
			if (entityId != null) {
				Key key = KeyFactory.createKey(ancestorKey, entityName,
						entityId);
				ReflectUtil.setFieldValue(entity, keyField, key);
			}
		} else {
			// otherwise, it should be automatically generated.
			// But at least the ancestor key must be set. Then
			// the entity will be set in the right build group
			

			if (parentKeyField != null) {
				String ancestorKeyString = KeyFactory.keyToString(ancestorKey);
				ReflectUtil.setFieldValue(entity, parentKeyField, ancestorKeyString);
			}
		}
	}

	protected <U extends DatastoreEntity> void setKey(List<U> entities) {
		for (U entity : entities) {
//			if (entity.getKey() == null) {
//				entity.setKeyWithAncestor(getAncestorKey());
//			}
			setKey(entity);
		}
	}

//	/**
//	 * Returns a list of all the entities of this kind (<em>T</em>) in the
//	 * datastore.
//	 * 
//	 * @return All the entities of kind <em>T</em> in datastore.
//	 */
//	public abstract List<T> findAll();
//
//	public abstract SearchResult<T> findOnePage(int page, int perPage);

//	/**
//	 * <p>
//	 * Find and return the entity with the given id. If the ID was generated
//	 * automatically, it might be a long instead of a String, and this method
//	 * would not find the entity.
//	 * </p>
//	 * <p>
//	 * Warning: The returned entity is detached. For instance, it can't be used
//	 * to remove it from the datastore.
//	 * </p>
//	 * 
//	 * @param id
//	 * @return the entity found or null if not found.
//	 */
//	public abstract T find(String id);
//
//	public abstract T findFromKeyString(String key);
//	
//	public abstract List<T> findFromKeyString(List<WithUniqueField> keys);

//	/**
//	 * <p>
//	 * Find and return the entity with the unique field.
//	 * </p>
//	 * <p>
//	 * Warning: The returned entity is detached. For instance, it can't be used
//	 * to remove it from the datastore.
//	 * </p>
//	 * 
//	 * @param id
//	 * @return
//	 */
//	public abstract <U extends WithUniqueField> WithUniqueField find(
//			UniqueField<U> field);
//	
//	abstract <U extends WithUniqueField> UniqueField<U> findUniqueField(
//			UniqueField<U> field);

//	/**
//	 * App Engine note: the performances are bad with the current implementation
//	 * <code>query.setFilter(":p.contains(key)");</code> - one query for each
//	 * key. Please avoid using this method if possible.
//	 * 
//	 * @param ids
//	 * @param page
//	 * @param perPage
//	 * @return
//	 */
//	public abstract SearchResult<T> findFromKeyStrings(List<String> ids,
//			int page, int perPage);

	/**
	 * App Engine note: the performances are bad with the current implementation
	 * <code>query.setFilter(":p.contains(key)");</code> - one query for each
	 * key. Please avoid using this method if possible.
	 * 
	 * @param ids
	 * @param page
	 * @param perPage
	 * @param totalResults
	 *            if the total number of results is already known. If not, you
	 *            may prefer to use {@link #findFromKeyStrings(List, int, int)}
	 * @return
	 */
	public abstract SearchResult<T> findFromKeyStrings(List<String> ids,
			int page, int perPage, long totalResults);

//	public abstract boolean exists(T entity);

	public SearchResult<T> search(String queryString, int page, int perPage) {
		SearchAPIResult searchAPIResult = sapi.search(queryString, page,
				perPage);
		if (searchAPIResult == null) {
			return null;
		} else {
			return this.find(searchAPIResult.getIds(), page,
					perPage, searchAPIResult.getTotalResults());
		}
	}

//	/**
//	 * Adds the given entity to the datastore. Its Id must be set and not be
//	 * already used in the datastore. If the id is already used, an error log is
//	 * writen.
//	 * 
//	 * @param entity
//	 *            The entity to add to the datastore.
//	 * @return True if the entity was successfully added to the datastore, false
//	 *         otherwise.
//	 */
//	public abstract boolean addOrUpdate(T entity);
//	public abstract boolean updateNoReindexingNoValidation(T entity);
//	public abstract boolean addOrUpdate(T entity, boolean indexes);

	public boolean addToIndex(T entity) {
		return sapi.add(entity);
	}

//	public abstract boolean addOrUpdate(List<T> entities);

	public boolean addToIndex(List<T> entities) {
		return sapi.add(entities);
	}

//	/**
//	 * Removes the entity with the given id in the string form.
//	 * 
//	 * @param id
//	 *            The id of the entity to delete. The method
//	 *            {@link KeyFactory#stringToKey(String)} is used to find the key
//	 *            from this String. It means that this id must have been
//	 *            generated using the method {@link KeyFactory#keyToString(Key)}
//	 *            .
//	 * @return
//	 */
//	public abstract T removeFromKeyString(String id);

	/**
	 * Removes the entity with the given key String ({@link String} form of its
	 * {@link Key}) from the index. To get a String from a Key, you may want to
	 * use {@link KeyFactory#keyToString(Key)}.
	 * 
	 * @param key
	 * @return
	 */
	public boolean removeFromIndex(String key) {
		return sapi.remove(key);
	}
	
//	public abstract void remove(T entity);
//
//	/**
//	 * Removes the entity with the given id which is the {@link String} form of
//	 * the <code>long</code> key id. To get a {@link String} id from a
//	 * {@link Key}, you may want to use {@link Key#getName()}. To convert a long
//	 * to a string id, you may want to create a key with
//	 * {@link KeyFactory#createKey(String, long)} and then get the String id.
//	 * 
//	 * @param id
//	 *            The {@link String} form of the id. Use the {@link KeyFactory}
//	 *            and the {@link Key} objects to convert the key id from long to
//	 *            {@link String}.
//	 * @return
//	 */
//	public abstract T remove(String id);

//	public abstract boolean remove(List<T> entities);

	public boolean removeFromIndex(List<String> ids) {
		return sapi.remove(ids);
	}

//	/**
//	 * Removes all members. This method can't be used within a transaction with
//	 * the current implementation because the PersistenceManager is
//	 * reinitialized.
//	 * 
//	 * @return
//	 */
//	public abstract boolean removeAll();

	public boolean removeAllFromIndex() {
		return sapi.clearIndexOfThisClass();
	}

	public boolean clearIndex() {
		return sapi.clearAllIndexes();
	}

	/**
	 * Updates an entity still attached to the datastore.
	 * 
	 * @param entity
	 *            The entity to update in the datastore
	 * @return
	 */
//	public abstract boolean update(T entity);

	// public abstract long getTotalResults();

//	/**
//	 * <p>
//	 * Get the object that can be used to do a transaction. Call then
//	 * {@link Transaction#begin()} to start the transaction,
//	 * {@link Transaction#commit()} to commit (end) the transaction and
//	 * {@link Transaction#rollback()} to rollback on finally block if
//	 * transaction is active ({@link Transaction#isActive()}).
//	 * </p>
//	 * <p>
//	 * Transactions are subject to restrictions on datastore (ie they might
//	 * throw {@link Exception}s) and might affect concurrency. So be careful
//	 * when using it.
//	 * </p>
//	 * 
//	 * @return The {@link Transaction} object that will be used to call
//	 *         {@link Transaction#begin()}, {@link Transaction#commit()},
//	 *         {@link Transaction#isActive()} and {@link Transaction#rollback()}
//	 *         methods.
//	 */
//	public abstract Transaction newTransactionObject();
//	
//	public abstract void beginTransaction(Transaction txn);
//	
//	public abstract void commitTransaction(Transaction txn);
//	
//	public abstract void rollbackTransaction(Transaction txn);

}
